import { Vector2 } from 'three';
import type { Object3D, Intersection, WebGLRenderer, Camera, Raycaster } from 'three';
import type { Ref } from 'vue';
import { computed, onUnmounted } from 'vue';
import type { EventHook } from '@vueuse/core';
import { createEventHook, useElementBounding, usePointer } from '@vueuse/core';


interface PointerMoveEventPayload {
    intersects: Intersection[];
    event: PointerEvent;
}
interface PointerClickEventPayload {
    intersects: Intersection[];
    event: PointerEvent;
}

const pointer = new Vector2();

export const useRaycaster = ( object: Ref<THREE.Object3D[] | THREE.Object3D>,
    { renderer, camera, raycaster }: { renderer: Ref<WebGLRenderer>, camera: Ref<Camera>, raycaster: Ref<Raycaster>; } ) => {

    const canvas = computed( () => renderer.value.domElement );

    const { x, y } = usePointer( { target: canvas } );

    const { width, height, top, left } = useElementBounding( canvas );

    const getRelativePointerPosition = ( { x, y }: { x: number; y: number; } ) => {
        if ( !canvas.value ) return;
        pointer.x = ( ( x - left.value ) / width.value ) * 2 - 1;
        pointer.y = -( ( y - top.value ) / height.value ) * 2 + 1;
        return pointer;
    };

    const getIntersectsByRelativePointerPosition = ( mouse: Vector2 ) => {

        if ( !camera.value ) return;

        raycaster.value.setFromCamera( mouse, camera.value );

        if ( Array.isArray( object.value ) ) {
            return raycaster.value.intersectObjects( object.value, false );
        } else {
            return raycaster.value.intersectObject( object.value, false );
        }

    };

    const getIntersects = ( event?: PointerEvent | MouseEvent ) => {
        const pointerPosition = getRelativePointerPosition( {
            x: event?.clientX ?? x.value,
            y: event?.clientY ?? y.value,
        } );
        if ( !pointerPosition ) return [];

        return getIntersectsByRelativePointerPosition( pointerPosition ) || [];
    };

    const intersects = computed( () => getIntersects() );

    const eventHookClick = createEventHook<PointerClickEventPayload>();
    const eventHookPointerMove = createEventHook<PointerMoveEventPayload>();

    const triggerEventHook = ( eventHook: EventHook, event: PointerEvent | MouseEvent ) => {
        eventHook.trigger( { event, intersects: getIntersects( event ) } );
    };

    const onPointerMove = ( event: PointerEvent ) => {
        triggerEventHook( eventHookPointerMove, event );
    };

    // a click event is fired whenever a pointerdown happened after pointerup on the same object

    let mouseDownObject: Object3D | undefined = undefined;

    const onPointerDown = ( event: PointerEvent ) => {
        mouseDownObject = getIntersects( event )[ 0 ]?.object;
    };

    const onPointerUp = ( event: MouseEvent ) => {
        if ( !( event instanceof PointerEvent ) ) return; // prevents triggering twice on mobile devices

        if ( mouseDownObject === getIntersects( event )[ 0 ]?.object ) triggerEventHook( eventHookClick, event );
    };

    const onPointerLeave = ( event: PointerEvent ) => eventHookPointerMove.trigger( { event, intersects: [] } );

    canvas.value.addEventListener( 'pointerup', onPointerUp );
    canvas.value.addEventListener( 'pointerdown', onPointerDown );
    canvas.value.addEventListener( 'pointermove', onPointerMove );
    canvas.value.addEventListener( 'pointerleave', onPointerLeave );

    onUnmounted( () => {
        if ( !canvas?.value ) return;
        canvas.value.removeEventListener( 'pointerup', onPointerUp );
        canvas.value.removeEventListener( 'pointerdown', onPointerDown );
        canvas.value.removeEventListener( 'pointermove', onPointerMove );
        canvas.value.removeEventListener( 'pointerleave', onPointerLeave );
    } );

    return {
        intersects,
        onClick: ( fn: ( value: PointerClickEventPayload ) => void ) => eventHookClick.on( fn ).off,
        onPointerMove: ( fn: ( value: PointerMoveEventPayload ) => void ) => eventHookPointerMove.on( fn ).off,
    };
};
